﻿using System;
using System.Globalization;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using NetActive.Notifications.Services.WP7.Interfaces;

namespace NetActive.Notifications.Services.WP7
{
    /// <summary>
    /// Sends push notifications to the Microsoft Push Notification Service.
    /// </summary>
    public class NotificationService : INotificationService
    {
        private int _maxNumberOfRetryAttempts;

        /// <summary>
        /// Default constructor for NotificationService.
        /// </summary>
        public NotificationService()
        {
            //set default number of retry attempts
            MaxNumberOfRetryAttempts = 10;
        }

        #region Public Events

        /// <summary>
        /// This event is raised, when the channel registered for the given recipient is expired.
        /// The handler of this event should drop the subscription state for its corresponding application session.
        /// </summary>
        public event ChannelExpiredEventHandler ChannelExpired;
        public delegate void ChannelExpiredEventHandler(object sender, NotificationCompletedEventArgs e);

        /// <summary>
        /// This event is raised, when the maximum number of retry attempts has exceeded without sending the notification.
        /// </summary>
        public event MaxNumberOfRetryAttemptsExceededEventHandler MaxNumberOfRetryAttemptsExceeded;
        public delegate void MaxNumberOfRetryAttemptsExceededEventHandler(object sender, NotificationCompletedEventArgs e);

        /// <summary>
        /// This event is raised, when a notification was sent successfully.
        /// </summary>
        public event NotificationSentEventHandler NotificationSent;
        public delegate void NotificationSentEventHandler(object sender, NotificationCompletedEventArgs e);

        #endregion

        /// <summary>
        /// Gets or sets the maximum number of times the service attempts to send a given notification.
        /// Please note after every failed attempt the delay is increased exponentialy.
        /// </summary>
        public int MaxNumberOfRetryAttempts
        {
            get { return _maxNumberOfRetryAttempts; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("value");

                _maxNumberOfRetryAttempts = value;
            }
        }

        /// <summary>
        /// Method used to asynchronously send the given <paramref name="notification"/>. Once completed the <see cref="INotificationService.NotificationSent"/> event is raised.
        /// This method automatically handles resending of the notification, if usefull.
        /// </summary>
        /// <param name="notification">Notification to send.</param>
        public void Send(INotification notification)
        {
            //no need to send the notification, if we have no channel Uri
            if (notification.ClientNotificationChannelUri == null)
                return;

            //send the notification async, handle response when it returns
            var taskSendNotification = new Task<NotificationResponse>(() => DoSendNotification(notification));
            taskSendNotification.ContinueWith(t => HandleNotificationServiceResponse(t.Result));
            taskSendNotification.Start();
        }

        /// <summary>
        /// Method used to asynchronously send all given <paramref name="notifications"/>. For every individual notification, once completed the <see cref="INotificationService.NotificationSent"/> event is raised.
        /// This method automatically handles resending of notifications, if usefull.
        /// </summary>
        /// <param name="notifications">Notifications to send.</param>
        public void Send(INotification[] notifications)
        {
            foreach (var notification in notifications)
                Send(notification);
        }

        #region Private Helper Methods

        /// <summary>
        /// Sends the specified notification to the Microsoft Push Notification Service.
        /// </summary>
        /// <param name="notification">Notification to send.</param>
        /// <returns>Response object including initials notification and the response from the Push Notification Services.</returns>
        private static NotificationResponse DoSendNotification(INotification notification)
        {
            //init notification response
            var notificationResponse = new NotificationResponse
                                           {
                                               Notification = notification,
                                               NotificationSentAtUtc = DateTime.UtcNow,
                                               Status = string.Empty,
                                               ChannelStatus = string.Empty,
                                               DeviceConnectionStatus = string.Empty
                                           };
            try
            {
                // Get the URI that the Microsoft Push Notification Service returns to the push client when creating a notification channel.
                // Normally, a web service would listen for URIs coming from the web client and maintain a list of URIs to send
                // notifications out to.
                var sendRequest = (HttpWebRequest) WebRequest.Create(notification.ClientNotificationChannelUri);

                // Create an HTTPWebRequest that posts the toast notification to the Microsoft Push Notification Service.
                // HTTP POST is the only method allowed to send the notification.
                sendRequest.Method = WebRequestMethods.Http.Post;

                // The optional custom header X-MessageID uniquely identifies a notification message. 
                // If it is present, the same value is returned in the notification response. It must be a string that contains a UUID.
                if (notification.UUID.HasValue)
                    sendRequest.Headers.Add("X-MessageID", notification.UUID.Value.ToString());

                // Set the notification payload to send.
                var notificationBytes = notification.GetBytes();

                // Set the web request content length.
                sendRequest.ContentLength = notificationBytes.Length;
                sendRequest.ContentType = "text/xml";
                sendRequest.Headers.Add("X-WindowsPhone-Target", notification.TargetHeader);
                sendRequest.Headers.Add("X-NotificationClass", notification.GetDeliveryInterval().ToString(CultureInfo.InvariantCulture));

                using (var requestStream = sendRequest.GetRequestStream())
                    requestStream.Write(notificationBytes, 0, notificationBytes.Length);

                // Send the notification and get the response.
                var response = (HttpWebResponse) sendRequest.GetResponse();

                notificationResponse.ApplyWebResponse(response);
            }
            catch (WebException wex)
            {
                notificationResponse.ApplyWebException(wex);
            }
            catch (Exception ex)
            {
                notificationResponse.ApplyException(ex);
            }

            //increase number of attempts to send the notification
            notificationResponse.Notification.NumberOfAttempts++;

            return notificationResponse;
        }

        /// <summary>
        /// Handles a response from the notification service. Resends notification if needed.
        /// </summary>
        /// <param name="response">Response from the notification service.</param>
        private void HandleNotificationServiceResponse(NotificationResponse response)
        {
            /* ChannelStatus (Active and Expired)
             * If a device's subscription has expired, you can remove its URI from your list of subscribed devices.
             * 
             * Status (Received, Dropped, or QueueFull)
             * You could use this value to determine whether you need to resend the notification to this device.
             * 
             * DeviceConnectionStatus (Connected, InActive, Disconnected, and TempDisconnected)
             * If it is important that a device receive the notification, you can use this value to determine whether 
             * you need to resend the notification later. */

            //if the notification was received, raise event
            if (response.Status.Equals("Received", StringComparison.CurrentCultureIgnoreCase))
            {
                if(NotificationSent != null)
                    NotificationSent(this, new NotificationCompletedEventArgs(response));

                return; //don't retry send notification
            }

            //if notification channel is expired, raise event
            if (response.ChannelStatus.Equals("Expired", StringComparison.CurrentCultureIgnoreCase))
            {
                if (ChannelExpired != null)
                    ChannelExpired(this, new NotificationCompletedEventArgs(response));

                return; //don't retry send notification
            }

            //don't allow more than X attempts
            if (response.Notification.NumberOfAttempts >= MaxNumberOfRetryAttempts)
            {
                if (MaxNumberOfRetryAttemptsExceeded != null)
                    MaxNumberOfRetryAttemptsExceeded(this, new NotificationCompletedEventArgs(response));

                return; //don't retry send notification
            }

            //default to a one minute delay
            var delayInMinutes = 1;

            if (response.Status.Equals("QueueFull", StringComparison.CurrentCultureIgnoreCase))
            {
                //if queue was full, retry send notification with increasing delay
                delayInMinutes = (int) Math.Pow(2, response.Notification.NumberOfAttempts - 1);
            }
            else if (response.Status.Equals("Dropped", StringComparison.CurrentCultureIgnoreCase))
            {
                //if notification was dropped, retry send notification with fixed delay of one hour
                delayInMinutes = 60;
            }
            else if (response.Error != null)
            {
                //Unexpected error (could be 503 Service Unavailable), retry send notification with increasing delay
                delayInMinutes = (int) Math.Pow(2, response.Notification.NumberOfAttempts - 1);
            }

            //resend the notification with a delay (as specified by the service)
            var task = RunDelayed(delayInMinutes, () => DoSendNotification(response.Notification));
            task.ContinueWith(t => HandleNotificationServiceResponse(t.Result));
        }

        /// <summary>
        /// Runs a delayed task.
        /// </summary>
        /// <typeparam name="T">Callback method.</typeparam>
        /// <param name="delayInMinutes">Delay in minutes.</param>
        /// <param name="functionToExecuteAfterDelay">Function to execute after delay.</param>
        /// <returns></returns>
        private static Task<T> RunDelayed<T>(int delayInMinutes, Func<T> functionToExecuteAfterDelay)
        {
            if (functionToExecuteAfterDelay == null)
                throw new ArgumentNullException("functionToExecuteAfterDelay");

            if (delayInMinutes < 0)
                throw new ArgumentOutOfRangeException("delayInMinutes");

            var taskCompletionSource = new TaskCompletionSource<T>();

            var timer = new Timer(self =>
                                      {
                                          ((Timer) self).Dispose();
                                          try
                                          {
                                              var result = functionToExecuteAfterDelay();
                                              taskCompletionSource.SetResult(result);
                                          }
                                          catch (Exception exception)
                                          {
                                              taskCompletionSource.SetException(exception);
                                          }
                                      });
            timer.Change(delayInMinutes*60*1000, delayInMinutes*1000);

            return taskCompletionSource.Task;
        }

        #endregion
    }
}